﻿namespace Home.Services.ProductService
{
    using System;
    using System.Collections.Generic;
    using System.Configuration;
    using System.Data.Entity;
    using System.Globalization;
    using System.IO;
    using System.Linq;
    using System.Security.Cryptography.X509Certificates;
    using System.Text;
    using System.Threading;
    using System.Threading.Tasks;
    using System.Transactions;
    using System.Web.Http;
    using System.Web.OData;
    using System.Web.OData.Query;
    using System.Web.OData.Routing;
    using CsvHelper;
    using CsvHelper.Configuration;
    using Home.Data;
    using Home.Security;
    using Home.Services.SecurityService;
    using Home.Services.UtilityService;
    using WebsiteProductType = Home.WebsiteProductWebServiceReference.ProductType;
    using WebsiteProductUploadType = Home.WebsiteProductWebServiceReference.ProductUploadType;
    using WebsiteProductWebService = Home.WebsiteProductWebServiceReference.ProductWebServiceClient;

    [OutputCache(NoCache = true)]
    [ServiceHandleError(ServiceName = ClientServiceHandleErrorHelper.ProductServiceName)]
    public class ProductController : BaseODataController
    {
        private readonly ProductDbContext database = new ProductDbContext();
        private static readonly X509Certificate2 encryptionCertificate = SecurityHelper.FindCertificate(StoreLocation.LocalMachine, StoreName.My, X509FindType.FindByThumbprint, ConfigurationManager.AppSettings["home:EncryptionCertificateThumbprint"]);

        #region Diamond import operations

        // GET odata/DiamondImports
        [ClientServiceAuthorize]
        [HttpGet]
        [EnableQuery(PageSize = 50)]
        [ODataRoute("DiamondImports")]
        public IHttpActionResult GetDiamondImport(ODataQueryOptions<DiamondImport> options)
        {
            var imports = this.database.DiamondImports.AsQueryable();
            var expands = options.GetExpandPropertyNames();
            if (expands.Contains("Products")) imports = imports.Include(s => s.Products);

            return Ok(imports);
        }

        // GET odata/DiamondImports('id')
        [ClientServiceAuthorize]
        [HttpGet]
        [EnableQuery]
        [ODataRoute("DiamondImports({id})")]
        public Task<IHttpActionResult> GetDiamondImport([FromODataUri] Guid id, ODataQueryOptions<DiamondImport> options)
        {
            var imports = this.database.DiamondImports.Where(s => s.Id == id);
            var expands = options.GetExpandPropertyNames();
            if (expands.Contains("Products")) imports = imports.Include(s => s.Products);

            return GetODataSingleAsync(imports, options);
        }

        // GET odata/DiamondImports('id')/Property
        [ClientServiceAuthorize]
        [HttpGet]
        [ODataRoute("DiamondImports({id})/Count")]
        [ODataRoute("DiamondImports({id})/Type")]
        [ODataRoute("DiamondImports({id})/Created")]
        [ODataRoute("DiamondImports({id})/CreatedBy")]
        [ODataRoute("DiamondImports({id})/Modified")]
        [ODataRoute("DiamondImports({id})/ModifiedBy")]
        public async Task<IHttpActionResult> GetDiamondImportProperty([FromODataUri] Guid id)
        {
            var import = await this.database.DiamondImports.FindAsync(id);
            return GetODataProperty(import);
        }

        // POST odata/DiamondImports
        [ClientServiceAuthorize(Roles = SecurityServiceHelper.RoleConstants.Administrator)]
        [HttpPost]
        [ODataRoute("DiamondImports")]
        public async Task<IHttpActionResult> PostDiamondImport(DiamondImport import)
        {
            if (!ModelState.IsValid)
            {
                return BadRequest(ModelState);
            }

            return Created(await CreateAsync(import));
        }

        // PUT odata/DiamondImports('id')
        [ClientServiceAuthorize(Roles = SecurityServiceHelper.RoleConstants.Administrator)]
        [HttpPut]
        [ODataRoute("DiamondImports({id})")]
        public Task<IHttpActionResult> PutDiamondImport([FromODataUri] Guid id, DiamondImport import, ODataQueryOptions<DiamondImport> options)
        {
            if (!ModelState.IsValid)
            {
                return Task.FromResult((IHttpActionResult)BadRequest(ModelState));
            }

            if (id != import.Id)
            {
                return Task.FromResult((IHttpActionResult)BadRequest("The Id of import does not match the id"));
            }

            var imports = this.database.DiamondImports.Where(r => r.Id == id);

            return PutOrPatchODataAsync(imports, options,
                () => CreateAsync(import),
                () => UpdateAsync(import));
        }

        // PATCH odata/DiamondImports('id')
        [ClientServiceAuthorize(Roles = SecurityServiceHelper.RoleConstants.Administrator)]
        [HttpPatch]
        [ODataRoute("DiamondImports({id})")]
        public Task<IHttpActionResult> PatchDiamondImport([FromODataUri] Guid id, Delta<DiamondImport> patch, ODataQueryOptions<DiamondImport> options)
        {
            if (!ModelState.IsValid)
            {
                return Task.FromResult((IHttpActionResult)BadRequest(ModelState));
            }

            var imports = this.database.DiamondImports.Where(r => r.Id == id);

            return PutOrPatchODataAsync(imports, options,
                () =>
                {
                    return CreatePatchAsync(patch);
                },
                async () =>
                {
                    return await UpdatePatchAsync(await imports.FirstAsync(), patch);
                });
        }

        // DELETE odata/DiamondImports('id')
        [ClientServiceAuthorize(Roles = SecurityServiceHelper.RoleConstants.Administrator)]
        [HttpDelete]
        [ODataRoute("DiamondImports({id})")]
        public Task<IHttpActionResult> DeleteDiamondImport([FromODataUri] Guid id, ODataQueryOptions<DiamondImport> options)
        {
            var imports = this.database.DiamondImports.Where(r => r.Id == id);
            return DeleteODataAsync(imports, options, () => imports.First().DeleteAsync(this.database, this.database.DiamondImports));
        }

        #endregion

        #region Diamond operations

        // GET odata/Diamonds
        [ClientServiceAuthorize]
        [HttpGet]
        [EnableQuery(PageSize = 50)]
        [ODataRoute("Diamonds")]
        public IHttpActionResult GetDiamond(ODataQueryOptions<Diamond> options)
        {
            var diamonds = this.database.Diamonds.AsQueryable();
            return Ok(diamonds);
        }

        // GET odata/Diamonds('id')
        [ClientServiceAuthorize]
        [HttpGet]
        [ODataRoute("Diamonds({id})")]
        public Task<IHttpActionResult> GetDiamond([FromODataUri] Guid id, ODataQueryOptions<Diamond> options)
        {
            var diamonds = this.database.Diamonds.Where(s => s.Id == id);
            return GetODataSingleAsync(diamonds, options);
        }

        // GET odata/Diamonds('id')/Property
        [ClientServiceAuthorize]
        [HttpGet]
        [ODataRoute("Diamonds({id})/Type")]
        [ODataRoute("Diamonds({id})/Shape")]
        [ODataRoute("Diamonds({id})/ReportType")]
        [ODataRoute("Diamonds({id})/ReportNumber")]
        [ODataRoute("Diamonds({id})/Caret")]
        [ODataRoute("Diamonds({id})/Clarity")]
        [ODataRoute("Diamonds({id})/Color")]
        [ODataRoute("Diamonds({id})/ColoredColor")]
        [ODataRoute("Diamonds({id})/Cut")]
        [ODataRoute("Diamonds({id})/Polish")]
        [ODataRoute("Diamonds({id})/Symmetry")]
        [ODataRoute("Diamonds({id})/Fluorescence")]
        [ODataRoute("Diamonds({id})/Comment")]
        [ODataRoute("Diamonds({id})/Status")]
        [ODataRoute("Diamonds({id})/Cost")]
        [ODataRoute("Diamonds({id})/SalePrice")]
        [ODataRoute("Diamonds({id})/Vendor")]
        [ODataRoute("Diamonds({id})/Created")]
        [ODataRoute("Diamonds({id})/CreatedBy")]
        [ODataRoute("Diamonds({id})/Modified")]
        [ODataRoute("Diamonds({id})/ModifiedBy")]
        public async Task<IHttpActionResult> GetDiamondProperty([FromODataUri] Guid id)
        {
            var diamond = await this.database.Diamonds.FindAsync(id);
            return GetODataProperty(diamond);
        }

        // POST odata/Diamonds
        [ClientServiceAuthorize(Roles = SecurityServiceHelper.RoleConstants.Administrator)]
        [HttpPost]
        [ODataRoute("Diamonds")]
        public async Task<IHttpActionResult> PostDiamond(Diamond diamond)
        {
            if (!ModelState.IsValid)
            {
                return BadRequest(ModelState);
            }

            return Created(await CreateAsync(diamond));
        }

        // PUT odata/Diamonds('id')
        [ClientServiceAuthorize]
        [HttpPut]
        [ODataRoute("Diamonds({id})")]
        public Task<IHttpActionResult> PutDiamond([FromODataUri] Guid id, Diamond diamond, ODataQueryOptions<Diamond> options)
        {
            if (!ModelState.IsValid)
            {
                return Task.FromResult((IHttpActionResult)BadRequest(ModelState));
            }

            if (id != diamond.Id)
            {
                return Task.FromResult((IHttpActionResult)BadRequest("The Id of diamond does not match the id"));
            }

            var diamonds = this.database.Diamonds.Where(r => r.Id == id);

            return PutOrPatchODataAsync(diamonds, options,
                () => CreateAsync(diamond),
                () => UpdateAsync(diamond));
        }

        // PATCH odata/Diamonds('id')
        [ClientServiceAuthorize]
        [HttpPatch]
        [ODataRoute("Diamonds({id})")]
        public Task<IHttpActionResult> PatchDiamond([FromODataUri] Guid id, Delta<Diamond> patch, ODataQueryOptions<Diamond> options)
        {
            if (!ModelState.IsValid)
            {
                return Task.FromResult((IHttpActionResult)BadRequest(ModelState));
            }

            var diamonds = this.database.Diamonds.Where(r => r.Id == id);

            return PutOrPatchODataAsync(diamonds, options,
                () =>
                {
                    return CreatePatchAsync(patch);
                },
                async () =>
                {
                    return await UpdatePatchAsync(await diamonds.FirstAsync(), patch);
                });
        }

        // DELETE odata/Diamonds('id')
        [ClientServiceAuthorize(Roles = SecurityServiceHelper.RoleConstants.Administrator)]
        [HttpDelete]
        [ODataRoute("Diamonds({id})")]
        public Task<IHttpActionResult> DeleteDiamond([FromODataUri] Guid id, ODataQueryOptions<Diamond> options)
        {
            var diamonds = this.database.Diamonds.Where(r => r.Id == id);
            return DeleteODataAsync(diamonds, options, () => diamonds.First().DeleteAsync(this.database, this.database.Diamonds));
        }

        #endregion

        #region Upload operations

        // GET odata/Uploads
        [ClientServiceAuthorize]
        [HttpGet]
        [EnableQuery(PageSize = 50)]
        [ODataRoute("Uploads")]
        public IHttpActionResult GetUpload(ODataQueryOptions<ProductUpload> options)
        {
            var uploads = this.database.Uploads.AsQueryable();
            return Ok(uploads);
        }

        // GET odata/Uploads('id')
        [ClientServiceAuthorize]
        [HttpGet]
        [EnableQuery]
        [ODataRoute("Uploads({id})")]
        public Task<IHttpActionResult> GetUpload([FromODataUri] Guid id, ODataQueryOptions<ProductUpload> options)
        {
            var uploads = this.database.Uploads.Where(s => s.Id == id);
            return GetODataSingleAsync(uploads, options);
        }

        // GET odata/Uploads('id')/Property
        [ClientServiceAuthorize]
        [HttpGet]
        [ODataRoute("Uploads({id})/Count")]
        [ODataRoute("Uploads({id})/ProductType")]
        [ODataRoute("Uploads({id})/UploadType")]
        [ODataRoute("Uploads({id})/RequestId")]
        [ODataRoute("Uploads({id})/Status")]
        [ODataRoute("Uploads({id})/Created")]
        [ODataRoute("Uploads({id})/CreatedBy")]
        [ODataRoute("Uploads({id})/Modified")]
        [ODataRoute("Uploads({id})/ModifiedBy")]
        public async Task<IHttpActionResult> GetUploadProperty([FromODataUri] Guid id)
        {
            var upload = await this.database.Uploads.FindAsync(id);
            return GetODataProperty(upload);
        }

        // POST odata/Uploads
        [ClientServiceAuthorize(Roles = SecurityServiceHelper.RoleConstants.Administrator)]
        [HttpPost]
        [ODataRoute("Uploads")]
        public async Task<IHttpActionResult> PostUpload(ProductUpload upload)
        {
            if (!ModelState.IsValid)
            {
                return BadRequest(ModelState);
            }

            return Created(await CreateAsync(upload));
        }

        // PUT odata/Uploads('id')
        [ClientServiceAuthorize(Roles = SecurityServiceHelper.RoleConstants.Administrator)]
        [HttpPut]
        [ODataRoute("Uploads({id})")]
        public Task<IHttpActionResult> PutUpload([FromODataUri] Guid id, ProductUpload upload, ODataQueryOptions<ProductUpload> options)
        {
            if (!ModelState.IsValid)
            {
                return Task.FromResult((IHttpActionResult)BadRequest(ModelState));
            }

            if (id != upload.Id)
            {
                return Task.FromResult((IHttpActionResult)BadRequest("The Id of upload does not match the id"));
            }

            var uploads = this.database.Uploads.Where(r => r.Id == id);

            return PutOrPatchODataAsync(uploads, options,
                () => CreateAsync(upload),
                () => UpdateAsync(upload));
        }

        // PATCH odata/Uploads('id')
        [ClientServiceAuthorize(Roles = SecurityServiceHelper.RoleConstants.Administrator)]
        [HttpPatch]
        [ODataRoute("Uploads({id})")]
        public Task<IHttpActionResult> PatchUpload([FromODataUri] Guid id, Delta<ProductUpload> patch, ODataQueryOptions<ProductUpload> options)
        {
            if (!ModelState.IsValid)
            {
                return Task.FromResult((IHttpActionResult)BadRequest(ModelState));
            }

            var uploads = this.database.Uploads.Where(r => r.Id == id);

            return PutOrPatchODataAsync(uploads, options,
                () =>
                {
                    return CreatePatchAsync(patch);
                },
                async () =>
                {
                    return await UpdatePatchAsync(await uploads.FirstAsync(), patch);
                });
        }

        // DELETE odata/Uploads('id')
        [ClientServiceAuthorize(Roles = SecurityServiceHelper.RoleConstants.Administrator)]
        [HttpDelete]
        [ODataRoute("Uploads({id})")]
        public Task<IHttpActionResult> DeleteUpload([FromODataUri] Guid id, ODataQueryOptions<ProductUpload> options)
        {
            var uploads = this.database.Uploads.Where(r => r.Id == id);
            return DeleteODataAsync(uploads, options, () => uploads.First().DeleteAsync(this.database, this.database.Uploads));
        }

        [ClientServiceAuthorize]
        [HttpPost]
        [ODataRoute("Upload()")]
        public async Task<IHttpActionResult> Upload(ODataActionParameters parameters)
        {
            if (!ModelState.IsValid)
            {
                return BadRequest(ModelState);
            }

            var products = default(IEnumerable<dynamic>);
            var writer = default(Action<CsvWriter, object>);
            var productType = (ProductType)parameters["productType"];
            var uploadType = (ProductUploadType)parameters["uploadType"];
            var status = default(ProductUploadStatus);
            var requestId = SecurityHelper.NewRequestId(encryptionCertificate);

            try
            {
                {
                    using (var db = new ProductDbContext())
                    {
                        switch (productType)
                        {
                            case ProductType.Diamond:
                                products = await db.Diamonds.ToArrayAsync();
                                writer = WriteDiamond;
                                break;
                        }
                    }
                }

                var content = new StringBuilder();
                {
                    var textWriter = default(TextWriter);
                    var csvWriter = default(CsvWriter);
                    try
                    {
                        textWriter = new StringWriter(content);
                        csvWriter = new CsvWriter(textWriter, new CsvConfiguration()
                        {
                            Delimiter = ",",
                            HasHeaderRecord = false,
                            IgnoreHeaderWhiteSpace = false,
                            Quote = '\"',
                            QuoteAllFields = false,
                            QuoteNoFields = false,
                            ThrowOnBadData = true,
                            TrimFields = true,
                            Encoding = Encoding.Unicode,
                        });

                        foreach (var product in products)
                        {
                            csvWriter.WriteField(product.Index);
                            csvWriter.WriteField(product.Cost);
                            csvWriter.WriteField(product.SalePrice);
                            writer(csvWriter, product);
                            csvWriter.NextRecord();
                        }

                        await textWriter.FlushAsync();
                    }
                    finally
                    {
                        if (csvWriter != null) csvWriter.Dispose();
                        if (textWriter != null) textWriter.Dispose();
                    }
                }

                {
                    var service = new WebsiteProductWebService();
                    try
                    {
                        service.Open();
                        await service.UploadAsync(requestId, ConvertToWebsite(productType), ConvertToWebsite(uploadType), content.ToString());
                        service.Close();
                    }
                    catch
                    {
                        service.Abort();
                        throw;
                    }
                }

                status = ProductUploadStatus.Success;
            }
            catch
            {
                status = ProductUploadStatus.Failure;
                throw;
            }
            finally
            {
                using (var transaction = new TransactionScope(TransactionScopeOption.Required, new TransactionOptions() { IsolationLevel = IsolationLevel.ReadCommitted }, TransactionScopeAsyncFlowOption.Enabled))
                {
                    var id = Guid.NewGuid();
                    var now = DateTimeOffset.Now;
                    var user = Thread.CurrentPrincipal.Identity.Name;

                    using (var db = new UtilityDbContext())
                    {
                        var time = default(DateTime);
                        var guid = default(Guid);
                        var key = default(string);
                        SecurityHelper.TryParseRequestId(requestId, out time, out guid, out key);
                        db.Requests.Add(new RequestDescription()
                        {
                            Id = id,
                            Created = now,
                            CreatedBy = user,
                            Modified = now,
                            ModifiedBy = user,
                            Date = time,
                            Guid = guid,
                            Type = RequestType.ProductUpload
                        });
                        await db.SaveChangesAsync();
                    }

                    using (var db = new ProductDbContext())
                    {
                        db.Uploads.Add(new ProductUpload()
                        {
                            Id = Guid.NewGuid(),
                            Count = products.Count(),
                            UploadType = uploadType,
                            ProductType = productType,
                            RequestId = id,
                            Status = status,
                            Created = now,
                            CreatedBy = user,
                            Modified = now,
                            ModifiedBy = user
                        });
                        await db.SaveChangesAsync();
                    }

                    transaction.Complete();
                }
            }

            return Ok(new ProductUploadResult() { Id = requestId });
        }

        #endregion

        #region Diamond import helpers

        private async Task<DiamondImport> CreateAsync(DiamondImport entity)
        {
            var now = DateTimeOffset.UtcNow;
            var user = Thread.CurrentPrincipal.Identity.Name;

            entity.Create(this.database.DiamondImports, now, user, true);

            if (entity.Products != null)
            {
                foreach (var product in entity.Products)
                {
                    product.ImportId = entity.Id;
                    product.Create(this.database.Diamonds, now, user, true);
                }
            }

            await this.database.SaveChangesAsync();
            return entity;
        }

        private async Task<DiamondImport> UpdateAsync(DiamondImport entity)
        {
            var now = DateTimeOffset.UtcNow;
            var user = Thread.CurrentPrincipal.Identity.Name;

            if (entity.Products != null)
            {
                foreach (var product in entity.Products)
                {
                    product.ImportId = entity.Id;
                    product.Update(this.database, this.database.Diamonds, now, user);
                }
            }

            entity.Update(this.database, this.database.DiamondImports, now, user);

            await this.database.SaveChangesAsync();
            return entity;
        }

        private Task<DiamondImport> CreatePatchAsync(Delta<DiamondImport> patch)
        {
            var entity = new DiamondImport();
            patch.Patch(entity);
            return CreateAsync(entity);
        }

        private Task<DiamondImport> UpdatePatchAsync(DiamondImport entity, Delta<DiamondImport> patch)
        {
            patch.Patch(entity);
            return UpdateAsync(entity);
        }

        #endregion

        #region Diamond helpers

        private async Task<Diamond> CreateAsync(Diamond entity)
        {
            var now = DateTimeOffset.UtcNow;
            var user = Thread.CurrentPrincipal.Identity.Name;

            entity.Create(this.database.Diamonds, now, user, true);

            await this.database.SaveChangesAsync();
            return entity;
        }

        private async Task<Diamond> UpdateAsync(Diamond entity)
        {
            var now = DateTimeOffset.UtcNow;
            var user = Thread.CurrentPrincipal.Identity.Name;

            entity.Update(this.database, this.database.Diamonds, now, user);

            await this.database.SaveChangesAsync();
            return entity;
        }

        private Task<Diamond> CreatePatchAsync(Delta<Diamond> patch)
        {
            var entity = new Diamond();
            patch.Patch(entity);
            return CreateAsync(entity);
        }

        private Task<Diamond> UpdatePatchAsync(Diamond entity, Delta<Diamond> patch)
        {
            patch.Patch(entity);
            return UpdateAsync(entity);
        }

        #endregion

        #region Upload helpers

        private async Task<ProductUpload> CreateAsync(ProductUpload entity)
        {
            var now = DateTimeOffset.UtcNow;
            var user = Thread.CurrentPrincipal.Identity.Name;

            entity.Create(this.database.Uploads, now, user, true);

            await this.database.SaveChangesAsync();
            return entity;
        }

        private async Task<ProductUpload> UpdateAsync(ProductUpload entity)
        {
            var now = DateTimeOffset.UtcNow;
            var user = Thread.CurrentPrincipal.Identity.Name;

            entity.Update(this.database, this.database.Uploads, now, user);

            await this.database.SaveChangesAsync();
            return entity;
        }

        private Task<ProductUpload> CreatePatchAsync(Delta<ProductUpload> patch)
        {
            var entity = new ProductUpload();
            patch.Patch(entity);
            return CreateAsync(entity);
        }

        private Task<ProductUpload> UpdatePatchAsync(ProductUpload entity, Delta<ProductUpload> patch)
        {
            patch.Patch(entity);
            return UpdateAsync(entity);
        }

        private static WebsiteProductType ConvertToWebsite(ProductType value)
        {
            switch (value)
            {
                case ProductType.Diamond: return WebsiteProductType.Diamond;
                default: throw new InvalidOperationException();
            }
        }

        private static WebsiteProductUploadType ConvertToWebsite(ProductUploadType value)
        {
            switch (value)
            {
                case ProductUploadType.Append: return WebsiteProductUploadType.Append;
                case ProductUploadType.Replace: return WebsiteProductUploadType.Replace;
                case ProductUploadType.Update: return WebsiteProductUploadType.Update;
                case ProductUploadType.AppendOrUpdate: return WebsiteProductUploadType.AppendOrUpdate;
                default: throw new InvalidOperationException();
            }
        }

        private static void WriteDiamond(CsvWriter writer, object product)
        {
            var p = (Diamond)product;
            writer.WriteField(p.Type.ToString());
            writer.WriteField(p.Shape.ToString());
            writer.WriteField(p.ReportType.ToString());
            writer.WriteField(p.ReportNumber);
            writer.WriteField(p.Caret.ToString());
            writer.WriteField(p.Clarity.ToString());
            writer.WriteField(p.Color.ToString());
            writer.WriteField(p.ColoredColor ?? string.Empty);
            writer.WriteField(p.Cut.ToString());
            writer.WriteField(p.Polish.ToString());
            writer.WriteField(p.Symmetry.ToString());
            writer.WriteField(p.Fluorescence ?? string.Empty);
            writer.WriteField(p.Comment ?? string.Empty);
        }

        #endregion

        #region Helpers

        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                this.database.Dispose();
            }

            base.Dispose(disposing);
        }

        #endregion
    }
}